<?php

namespace app\admin\controller\helper;

use app\common\controller\Backend;
use app\admin\model\AuthRule;
use think\Exception;
use think\exception\ErrorException;
use think\Config;
use think\Db;
use fast\Tree;

/**
 * 一键生成工具
 *
 * @icon fa fa-file-code-o
 */
class Generator extends Backend
{
    protected $noNeedRight = ['getFieldList'];

    public function _initialize()
    {
        parent::_initialize();
    }

    public function index()
    {
        if ($this->request->isPost()) {
            $local    = $this->request->post('local');
            $force    = $this->request->post('force');
            $table    = $this->request->post('table');
            $menu     = $this->request->post('menu');
            $tabField = $this->request->post('tabField');
            $list     = $this->request->post('list/a');
            if (!$menu || !$table) {
                $this->error('生成参数不正确');
            }
            $modelModuleName = $local ? 'admin' : 'common';
            // 控制器信息
            list($controllerNamespace, $controllerName, $controllerFile, $controllerArr) = $this->getParseNameData($modelModuleName, $menu, 'controller');
            // 模型信息
            list($modelNamespace, $modelName, $modelFile, $modelArr) = $this->getParseNameData($modelModuleName, $menu, 'model');

            // 非覆盖模式时如果存在控制器文件则报错
            if (is_file($controllerFile) && !$force) {
                $this->error('控制器已存在!<br/>若要强制重新生成，请选择<code>覆盖模式</code>。');
            }
            // list列表页配置
            $list['imageField'] = isset($list['imageField']) ? $list['imageField'] : false;
            // 处理字段配置
            $field = $this->request->post('field/a', []);
            $rules = [];
            $fieldContent = [];
            foreach ($field as $k => $v) {
                // 剔除任何页面都不显示的字段
                if (!isset($v['show'])) {
                    unset($field[$k]);
                    continue;
                } else {
                    unset($field[$k]['show']);
                }
                // 生成表单验证规则
                if (isset($v['rules'])) {
                    $rules[$k] = [];
                    foreach ($v['rules'] as $rule) {
                        $rules[$k][$rule] = true;
                    }
                    unset($field[$k]['rules']);
                }
                $field[$k]['sortable']   = $field[$k]['sortable'] ? true : false;
                $field[$k]['filterable'] = $field[$k]['filterable'] ? true : false;
                $field[$k]['viewable']   = $field[$k]['viewable'] ? true : false;
                $field[$k]['editable']   = $field[$k]['editable'] ? true : false;
                $field[$k]['addable']    = $field[$k]['addable'] ? true : false;
                // 生成select选项(fieldContent)
                if (in_array($v['type'], ['select', 'selects', 'switch'])) {
                    $fieldInfo = $this->getFieldInfo($table, $k);
                    $fieldContent[$k] = $this->getFieldArray($fieldInfo);
                }
            }
            // 权限配置
            $auth = [];
            $menu = str_replace('/', '.', rtrim(ltrim($menu, '/'), '/'));
            if (in_array('add', $this->request->post('auth/a', []))) {
                $auth['add'] = [
                    'name' => '添加',
                    'url'  => $menu . '/add'
                ];
            }
            if (in_array('edit', $this->request->post('auth/a', []))) {
                $auth['edit'] = [
                    'name' => '编辑',
                    'url'  => $menu . '/edit'
                ];
            }
            if (in_array('del', $this->request->post('auth/a', []))) {
                $auth['del'] = [
                    'name' => '删除',
                    'url'  => $menu . '/del'
                ];
                
                $auth['multi']['del'] = [
                    // 'del'  => [
                        'name'        => '删除',
                        'color'       => '#e74c3c',
                        'icon'        => 'fa-trash',
                        'url'         => $menu . '/del'
                    // ]
                ];
                $auth['more']['del'] = [
                    // 'del'  => [
                        'name'  => '删除',
                        'color' => '#e74c3c',
                        'url'   => $menu . '/del'
                    // ]
                ];
            }
            // 选项卡配置
            $tabList = NULL;
            if ($tabField) {
                $fieldInfo = $this->getFieldInfo($table, $tabField);
                $fieldArr = $this->getFieldArray($fieldInfo);
                $tabList[] = array('id' => 'all', 'name' => '全部');
                foreach ($fieldArr as $k => $v) {
                    $tabList[] = array('id' => strval($k), 'name' => $v);
                }
            }
            // 合并全部配置
            $config = [
                'model'             => $this->request->post('model') ?: '记录',
                'limit'             => 10,      // 默认每页记录数
                'defaultSort'       => 'id',    // 默认排序字段
                'defaultOrder'      => 'desc',  // 默认排序方向
                'search'            => in_array('search', $this->request->post('buttons/a', [])),
                'filter'            => in_array('filter', $this->request->post('buttons/a', [])),
                'multi'             => in_array('multi', $this->request->post('buttons/a', [])),
                'sort'              => in_array('sort', $this->request->post('buttons/a', [])),
                'searchPlaceholder' => '输入ID搜索',    // 默认搜索框placeholder
                'tabField'          => $tabField ? $tabField : false,
                'list'              => $list,
                'field'             => $field,
                'fieldContent'      => $fieldContent,
                'rules'             => $rules,
                'auth'              => $auth,
            ];

            $data = [
                'controllerNamespace' => $controllerNamespace,
                'controllerName'      => $controllerName,
                'modelNamespace'      => $modelNamespace,
                'modelName'           => $modelName,
                'config'              => $this->varExport($config,'    ',true),
                'tabList'             => $this->varExport($tabList,'    ',true)
            ];

            // 生成控制器文件
            try {
                $this->writeToFile('controller', $data, $controllerFile);
            } catch (ErrorException $e) {
                throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile());
            }
            $this->success('生成成功');
        }
        // 数据表列表
        $tableList = [];
        $list = \think\Db::query('SHOW TABLES');
        foreach ($list as $key => $row) {
            $tableList[reset($row)] = reset($row);
        }
        // 菜单规则列表
        $ruleList = collection(AuthRule::field('condition,remark,createtime,updatetime', true)->where('ismenu', 1)->where('id', '<>', 1)->order('weigh DESC,id ASC')->select())->toArray();
        foreach ($ruleList as $k => &$v) {
            $v['title'] = __($v['title']);
        }
        unset($v);
        Tree::instance()->init($ruleList);
        $this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
        $ruledata = [];
        foreach ($this->rulelist as $k => &$v) {
            $ruledata[] = [
                'id'       => $v['id'],
                'title'    => $v['title'],
                'rule'     => $v['name'],
                'haschild' => $v['haschild']
            ];
        }
        unset($v);
        $this->view->assign('ruledata', $ruledata);
        $this->view->assign('tableList', $tableList);
        return $this->view->fetch();
    }

    /**
     * 获取字段列表并自动识别
     * @internal
     */
    public function getFieldList()
    {
        $dbname = Config::get('database.database');
        $table  = $this->request->request('table');
        //从数据库中获取表字段信息
        $sql = "SELECT * FROM `information_schema`.`columns` "
            . "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? "
            . "ORDER BY ORDINAL_POSITION";
        //加载主表的列
        $columnList = Db::query($sql, [$dbname, $table]);
        $fieldList = [];
        $tabList = [];
        foreach ($columnList as $index => $item) {
            if ($item['COLUMN_NAME'] === 'deletetime') {
                continue;
            }
            $fieldList[] = [
                'COLUMN_NAME'    => $item['COLUMN_NAME'],
                'DATA_TYPE'      => $item['DATA_TYPE'],
                'COLUMN_TYPE'    => $item['COLUMN_TYPE'],
                'COLUMN_DEFAULT' => $item['COLUMN_DEFAULT'],
                'COLUMN_COMMENT' => $item['COLUMN_COMMENT'],
                'label'          => $this->getFieldLabel($item['COLUMN_COMMENT']),
                'type'           => $this->getFieldType($item),
            ];
            if ($item['DATA_TYPE'] === 'enum') {
                $tabList[] = [
                    'name' => $item['COLUMN_NAME'],
                    'value' => $item['COLUMN_NAME'],
                ];
            }
        }
        // 主表记录名
        $TABLE_COMMENT = $this->getRecordName($table);
        $this->success('', NULL, ['fieldList' => $fieldList, 'tabList' => $tabList, 'TABLE_COMMENT' => $TABLE_COMMENT]);
    }
    /**
     * 获取某个字段的信息
     * @param [string] $table 表名
     * @param [string] $name 字段名
     * @return array
     */
    protected function getFieldInfo($table, $name)
    {
        $dbname = Config::get('database.database');
        //从数据库中获取表字段信息
        $sql = "SELECT * FROM `information_schema`.`columns` "
            . "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? "
            . "ORDER BY ORDINAL_POSITION";
        //加载主表的列
        $result = Db::query($sql, [$dbname, $table, $name]);
        return $result[0];
    }
    /**
     * 获取字段标题
     * @param [string] $comment 字段注释
     * @return string
     */
    protected function getFieldLabel($comment)
    {
        $comment = str_replace('：', ':', $comment);
        if (stripos($comment, ':') !== false) {
            return strstr($comment,  ':',  true);
        } else {
            return $comment;
        }
    }
    /**
     * 获取字段的选项数组
     * @param [array] $field 字段信息
     * @return array
     */
    protected function getFieldArray($field)
    {
        $item = substr($field['COLUMN_TYPE'], strlen($field['DATA_TYPE']) + 1, -1);
        $item = explode(',', str_replace("'", '', $item));
        $itemArr = [];
        $comment = str_replace('，', ',', $field['COLUMN_COMMENT']);
        if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
            list($fieldLang, $item) = explode(':', $comment);
            foreach (explode(',', $item) as $k => $v) {
                $valArr = explode('=', $v);
                if (count($valArr) == 2) {
                    list($key, $value) = $valArr;
                    $itemArr[strval($key)] = $value;
                }
            }
        } else {
            foreach ($item as $k => $v) {
                $itemArr[$v] = is_numeric($v) ? $field['COLUMN_NAME'] . ' ' . $v : $v;
            }
        }
        return $itemArr;
    }
    /**
     * 获取组件类型
     * @param [array] $type 数据库column信息
     * @return string
     */
    protected function getFieldType($column)
    {
        $inputType = 'text';
        $typeList  = ['text','password','idcard','image','images','file','files','textarea','editor'];
        $opList    = ['=','!=','LIKE','NOT LIKE','IN','NOT IN','FIND_IN_SET','NULL','NOT NULL'];
        switch ($column['DATA_TYPE']) {
            case 'bigint':
            case 'int':
            case 'mediumint':
            case 'smallint':
            case 'tinyint':
                $inputType = 'number';
                $typeList  = ['number','year','datetime','switch'];
                $opList    = ['=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
                break;
            case 'enum':
            case 'set':
                $inputType = 'select';
                $typeList  = ['select','switch'];
                $opList    = ['=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
                break;
            case 'decimal':
            case 'double':
            case 'float':
                $inputType = 'digit';
                $typeList  = ['digit'];
                $opList    = ['=','!=','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
                break;
            case 'longtext':
            case 'text':
            case 'mediumtext':
            case 'smalltext':
            case 'tinytext':
                $inputType = 'textarea';
                $typeList  = ['textarea','editor'];
                $opList    = ['=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
                break;
            case 'year':
                $inputType = 'year';
                $typeList  = ['year','number'];
                $opList    = ['=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
                break;
            case 'date':
                $inputType = 'date';
                $typeList  = ['date','text'];
                $opList    = ['=','!=','>','<','>=','<=','IN','NOT IN','RANGE','NOT RANGE','NULL','NOT NULL'];
                break;
            case 'time':
                $inputType = 'time';
                $typeList  = ['time','text'];
                $opList    = ['=','!=','>','<','>=','<=','IN','NOT IN','RANGE','NOT RANGE','NULL','NOT NULL'];
                break;
            case 'datetime':
            case 'timestamp':
                $inputType = 'datetime';
                $typeList  = ['datetime','text'];
                $opList    = ['=','!=','>','<','>=','<=','IN','NOT IN','RANGE','NOT RANGE','NULL','NOT NULL'];
                break;
            default:
                break;
        }
        $fieldsName = $column['COLUMN_NAME'];
        // 指定后缀说明是个时间字段后缀
        if ($this->isMatchSuffix($fieldsName, ['time'])) {
            $inputType = 'datetime';
            $typeList  = ['datetime'];
            $opList    = ['=','!=','>','<','>=','<=','IN','NOT IN','RANGE','NOT RANGE','NULL','NOT NULL'];
        }
        // 指定后缀结尾且类型为enum,说明是个单选框
        if ($this->isMatchSuffix($fieldsName, ['data', 'state', 'status']) && $column['DATA_TYPE'] == 'enum') {
            $inputType = "select";
            $typeList  = ['select'];
            $opList    = ['=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','BETWEEN','NOT BETWEEN','NULL','NOT NULL'];
        }
        // 指定后缀结尾且类型为set,说明是个复选框
        if ($this->isMatchSuffix($fieldsName, ['data', 'state', 'status', 'flag']) && $column['DATA_TYPE'] == 'set') {
            $inputType = "selects";
            $typeList  = ['selects'];
            $opList    = ['FIND_IN_SET','=','!=','LIKE','NOT LIKE','>','<','>=','<=','IN','NOT IN','NULL','NOT NULL'];
        }
        // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch开关组件
        if ($this->isMatchSuffix($fieldsName, ['switch']) && ($column['COLUMN_TYPE'] == 'tinyint(1)' || $column['COLUMN_TYPE'] == 'char(1)') && $column['COLUMN_DEFAULT'] !== '' && $column['COLUMN_DEFAULT'] !== NULL) {
            $inputType = "switch";
            $typeList  = ['switch'];
            $opList    = ['=','!='];
        }
        // 指定密码后缀
        if ($this->isMatchSuffix($fieldsName, ['password'])) {
            $inputType = 'password';
            $typeList  = ['password','text'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        // 指定图片后缀
        if ($this->isMatchSuffix($fieldsName, ['image', 'avatar'])) {
            $inputType = 'image';
            $typeList  = ['image','text','textarea'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        // 指定图片(多)后缀
        if ($this->isMatchSuffix($fieldsName, ['images'])) {
            $inputType = 'images';
            $typeList  = ['images','text','textarea'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        // 指定文件后缀
        if ($this->isMatchSuffix($fieldsName, ['file'])) {
            $inputType = 'file';
            $typeList  = ['file','text','textarea'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        // 指定文件(多)后缀
        if ($this->isMatchSuffix($fieldsName, ['files'])) {
            $inputType = 'files';
            $typeList  = ['files','text','textarea'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        // 指定富文本(编辑器)后缀
        if ($this->isMatchSuffix($fieldsName, ['content']) && $column['DATA_TYPE'] == 'text' ) {
            $inputType = 'editor';
            $typeList  = ['editor','text','textarea'];
            $opList    = ['=','!=','LIKE','NOT LIKE','NULL','NOT NULL'];
        }
        $result = [
            'inputType' => $inputType,
            'typeList'  => $typeList,
            'opList'    => $opList
        ];
        return $result;
    }
    /**
     * 判断是否符合指定后缀
     * @param string $field     字段名称
     * @param mixed  $suffixArr 后缀
     * @return boolean
     */
    protected function isMatchSuffix($field, $suffixArr)
    {
        $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
        foreach ($suffixArr as $k => $v) {
            if (preg_match("/{$v}$/i", $field)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 获取表的单条记录名
     * @param [string] $table 表名
     * @return string
     */
    protected function getRecordName($table)
    {
        $dbname = Config::get('database.database');
        // 主表注释
        $sql = "SELECT `TABLE_COMMENT` FROM `information_schema`.`TABLES` "
            . "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ";
        $TABLE_COMMENT = Db::query($sql, [$dbname, $table]);
        $TABLE_COMMENT = $TABLE_COMMENT[0]['TABLE_COMMENT'];
        $TABLE_COMMENT = mb_substr($TABLE_COMMENT, -1) == '表' ? mb_substr($TABLE_COMMENT, 0, -1) : $TABLE_COMMENT;
        $TABLE_COMMENT = $TABLE_COMMENT === '' ? '记录' : $TABLE_COMMENT;
        return $TABLE_COMMENT;
    }
    /**
     * 获取已解析相关信息
     * @param string $module 模块名称
     * @param string $name   菜单路径
     * @param string $type   解析类型，本例中为controller、model
     * @return array
     */
    protected function getParseNameData($module, $name, $type)
    {
        $arr = [];
        $name = str_replace(['.', '/', '\\'], '/', $name);
        $arr = explode('/', $name);
        $parseName = ucfirst(array_pop($arr));
        $parseArr = $arr;
        array_push($parseArr, $parseName);
        if ($type === 'controller') {
            $parseNamespace = "addons\\helper\\controller\\wxapp" . ($arr ? "\\" . implode("\\", $arr) : "");
            $parseFile =  dirname($_SERVER['SCRIPT_FILENAME']) . DS . '../addons/helper/controller/wxapp' . DS . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php';
        }
        if ($type === 'model') {
            $appNamespace = Config::get('app_namespace');
            $parseNamespace = "{$appNamespace}\\{$module}\\{$type}";
            $moduleDir = APP_PATH . $module . DS;
            $parseFile = $moduleDir . $type . DS . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php';
        }
        return [$parseNamespace, $parseName, $parseFile, $parseArr];
    }
    /**
     * 写入到文件
     * @param string $name
     * @param array  $data
     * @param string $pathname
     * @return mixed
     */
    protected function writeToFile($name, $data, $pathname)
    {
        foreach ($data as $index => &$datum) {
            $datum = is_array($datum) ? '' : $datum;
        }
        unset($datum);
        $content = $this->getReplacedStub($name, $data);

        if (!is_dir(dirname($pathname))) {
            mkdir(dirname($pathname), 0755, true);
        }
        return file_put_contents($pathname, $content);
    }
    /**
     * 获取替换后的数据
     * @param string $name
     * @param array  $data
     * @return string
     */
    protected function getReplacedStub($name, $data)
    {
        foreach ($data as $index => &$datum) {
            $datum = is_array($datum) ? '' : $datum;
        }
        unset($datum);
        $search = $replace = [];
        foreach ($data as $k => $v) {
            $search[] = "{%{$k}%}";
            $replace[] = $v;
        }
        $stubname = $this->getStub($name);
        if (isset($this->stubList[$stubname])) {
            $stub = $this->stubList[$stubname];
        } else {
            $this->stubList[$stubname] = $stub = file_get_contents($stubname);
        }
        $content = str_replace($search, $replace, $stub);
        return $content;
    }
    /**
     * 获取基础模板
     * @param string $name
     * @return string
     */
    protected function getStub($name)
    {
        return __DIR__ . DS . 'Crud' . DS . 'stubs' . DS . $name . '.stub';
    }
    /**
     * 格式化var_export
     * @param mixed $var
     * @param string $indent 前置缩进
     * @param boolean $strKey 强制转换key为字符串
     * @return string
     */
    private function varExport($var, $indent = '', $strKey = false)
    {
        switch (gettype($var)) {
            case 'string':
                return '\'' . addcslashes(trim($var), "\\\$\\'\r\n\t\v\f") . '\'';
            case 'array':
                $indexed = array_keys($var) === range(0, count($var) - 1);
                $result = [];
                foreach ($var as $key => $value) {
                    if (!$strKey) {
                        $result[] = $indent . '    '
                        . ($indexed ? '' : $this->varExport($key,'',$strKey) . ' => ')
                        . $this->varExport($value, "$indent    ",$strKey);
                    } else {
                        $result[] = $indent . '    '
                        . $this->varExport(strval($key),'',$strKey) . ' => '
                        . $this->varExport($value, "$indent    ",$strKey);
                    }
                }
                return "[\n" . implode(",\n", $result) . ($result ? ',' : '') . "\n" . $indent . ']';
            case 'boolean':
                return $var ? 'true' : 'false';

            case 'NULL':
                return 'NULL';

            case 'object' && isset($var->express):
                return $var->express;

            default:
                return var_export($var, true);
        }
    }
}
